﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;

namespace OpGen.Compiler.Parser
{
    /// <summary>
    /// OpGen Protokol Parser
    /// </summary>
    public class OpGenXmlParser : IOpGenParser
    {
        private Stack<Element> _elementStack = new Stack<Element>();
        private XmlDocument _xd;

        private ProjectElement _project;
        private IOpGenParser _baseParser;
        private MessageItemCollection _errorList;

        /// <summary>
        /// Erstellt eine neue Instanz der OpGenParser-Klasse
        /// </summary>
        /// <param name="sourceFile">Datei die den Quellcode enthält</param>
        public OpGenXmlParser(FileInfo sourceFile)
        {
            Logger.Normal("Init Parser");

            this.SourceFile = sourceFile;
            _errorList = new MessageItemCollection();
            _project = new ProjectElement() { SourceFile = sourceFile };
            _baseParser = this;

            if (!this.SourceFile.Exists)
            {
                AddException(0, "Sourcefile not found ({0}]", this.SourceFile.Name);
                Logger.Error( "Sourcefile not found ({0}]", this.SourceFile.Name);
            }
        }

        /// <summary>
        /// Erstellt eine neue Instanz der OpGenParser-Klasse
        /// </summary>
        /// <param name="sourceFile">Datei die den Quellcode enthält</param>
        /// <param name="baseParser">Der Basisdatei Parser</param>
        /// <remarks>Wird verwendet wenn Include Dateien geladen werden</remarks>
        internal OpGenXmlParser(FileInfo sourceFile, IOpGenParser baseParser)
        {
            Logger.Normal("Init Include Parser");

            this.SourceFile = sourceFile;
            _errorList = new MessageItemCollection();
            _project = new ProjectElement() { SourceFile = sourceFile };

            if (!this.SourceFile.Exists)
            {
                AddException(0, "Sourcefile not found ({0}]", this.SourceFile.Name);
                Logger.Error("Sourcefile not found ({0}]", this.SourceFile.Name);
            }
        }

        /// <summary>
        /// Führt den Parser Vorgang aus
        /// </summary>
        /// <returns></returns>
        public bool Execute()
        {
            Logger.Normal("XML Parsing in process...");

            if (!SourceFile.Exists)
                return false;

            Project.SourceFile = SourceFile;

            _xd = new XmlDocument();
            try 
            { 
                _xd.Load(SourceFile.OpenText()); 
            }
            catch (Exception)
            { 
                Logger.Normal("Faild to load XML");
                return false; 
            }

            ParseProject();           

            ParseIncludeFiles();

            ParseNamespace();

            Logger.Normal("...Complet");

            ParseProjectPreferences();

            Logger.Normal("Create OpCode Enum");
            BuildCodeEnum(_xd.SelectSingleNode("opgen/preferences/codeenum"));
            
            // Referenzen suchen
            Recognizer();

            if (ErrorList.HasException)
                return false;

            return true;
        }
        
        /// <summary>
        /// Sucht nach den Include Dateien
        /// </summary>
        /// <param name="Filename">Dateiname aus der Quelldatei</param>
        /// <returns></returns>
        private FileInfo FindIncludeFile(string Filename)
        {
            var includePathStack = new Stack<string>();

            includePathStack.Push(Filename);
            includePathStack.Push(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Filename));
            includePathStack.Push(Path.Combine(SourceFile.DirectoryName, Filename));

            while (includePathStack.Count > 0)
            {
                var File = new FileInfo(includePathStack.Pop());

                if (File.Exists)
                    return File;
            }

            return new FileInfo(Filename);
        }

        private void ParseProject()
        {
            var projectNode = _xd.SelectSingleNode("opgen");

            Project.Projectname = projectNode.Attributes.GetAttribute<string>("name", Project.Elementname);
        }

        private void ParseProjectPreferences()
        {
            var perferenceNode = _xd.SelectSingleNode("opgen/preferences");

            if (perferenceNode != null)
            {
                XmlNode node;

                // Code Enum Builder
                node = perferenceNode.SelectSingleNode("codeenum");
                _project.BuildCodeEnum = node != null;

                // Extensions einfügen
                node = perferenceNode.SelectSingleNode("extensions");
                if (node != null && node.Attributes.GetAttribute<bool>("enabled", false))
                    _project.UseExtension = true;

                // C# Smart ausgabe
                node = perferenceNode.SelectSingleNode("smart");
                if (node != null && node.Attributes.GetAttribute<bool>("enabled", false))
                    _project.CSSmartMode=true;
            }
            
        }

        private void BuildCodeEnum(XmlNode opcodeEnumNode)
        {
            if (_project.BuildCodeEnum)
            {
                NamespaceElement enumNamespace;
                var namespaceName = opcodeEnumNode.Attributes.GetAttribute<string>("namespace", "OpCodes");

                // Namespace suchen oder erstellen
                if (_project.ChildElements.Contains(namespaceName))
                    enumNamespace = (NamespaceElement)_project.ChildElements[namespaceName];
                else
                {
                    enumNamespace = new NamespaceElement(_project) { Elementname = namespaceName };
                    _project.ChildElements.Add(enumNamespace);
                }
                
                _project.CodeEnumNamespace = enumNamespace;

                // Enum Element erstellen
                var enumElement = new EnumElement(enumNamespace) { Elementname = opcodeEnumNode.Attributes.GetAttribute<string>("name", "OpCodes") };

                // Namespace und Enum konfigurieren
                enumElement.Modifier = GetModifierType(opcodeEnumNode.Attributes.GetAttribute<string>("modifier", "public"));
                
                // Den Namespace dem Projekt hinzufügen
                enumNamespace.ChildElements.Add(enumElement);

                // Nach allen Message Objekten suchen
                foreach (NamespaceElement Namespace in _project.ChildElements)
                {
                    foreach (var element in Namespace.ChildElements)
                    {
                        if (element is MessageElement)
                        {
                            var codeEnumItem = new EnumItemElement(enumElement);
                            codeEnumItem.Elementname = element.Elementname;
                            codeEnumItem.Value = ((MessageElement)element).Code;
                            enumElement.ChildElements.Add(codeEnumItem);
                        }
                    }
                }
            }
        }

        private void ParseIncludeFiles()
        {
            foreach (XmlNode IncludeNode in _xd.SelectNodes("opgen/include"))
            {
                var filename = IncludeNode.InnerText;
                var file = FindIncludeFile(filename);

                Logger.Normal("Load Includefile '{0}'", file.Name);

                if (file.Exists)
                    BaseParser.Project.AddIncludeFile(file, BaseParser);
                else
                {
                    AddWarning(Row, "Include Datei ({0}) wurde nicht gefunden", file.Name);
                    Logger.Warning("Include Datei ({0}) wurde nicht gefunden", file.Name);
                }
            }
        }

        /// <summary>
        /// Analysiert eine Namespace Zeile
        /// </summary>
        private void ParseNamespace()
        {
            foreach (XmlNode namespaceNode in _xd.SelectNodes("opgen/namespace"))
            {
                var namespaceName = namespaceNode.Attributes.GetAttribute<string>("name", Project.Elementname);

                if (!string.IsNullOrEmpty(namespaceName))
                {
                    var element = new NamespaceElement(Project);
                    element.Elementname = namespaceName;
                    Project.ChildElements.Add(element);

                    ParseEnum(namespaceNode,element);
                    ParseClass(namespaceNode, element);
                    ParseMessage(namespaceNode, element);

                    Logger.Normal("Parsing Namespace {0}", element.Elementname);
                }
            }
        }

        /// <summary>
        /// Analysiert eine Enum Zeile
        /// </summary>
        /// <param name="namespaceNode"></param>
        /// <param name="parentElement"></param>
        private void ParseEnum(XmlNode namespaceNode, NamespaceElement parentElement)
        {
            foreach (XmlNode enumNode in namespaceNode.SelectNodes("enum"))
            {
                var enumName = enumNode.Attributes.GetAttribute<string>("name", "");
                var enumModifier = GetModifierType(enumNode.Attributes.GetAttribute<string>("modifier", ""));
                var enumFlag = enumNode.Attributes.GetAttribute<bool>("flag", false);

                if (!string.IsNullOrEmpty(enumName))
                {
                    var element = new EnumElement(parentElement);
                    element.Elementname = enumName;
                    element.Modifier = enumModifier;
                    element.IsFlag = enumFlag;

                    ParseEnumItem(enumNode, element);

                    Logger.Normal("Parsing Enum {0}", element.Elementname);
                    parentElement.ChildElements.Add(element);
                }
            }
        }

        /// <summary>
        /// Analysiert eine Class Zeile
        /// </summary>
        /// <param name="namespaceNode"></param>
        /// <param name="parentElement"></param>
        private void ParseClass(XmlNode namespaceNode, NamespaceElement parentElement)
        {
            foreach (XmlNode classNode in namespaceNode.SelectNodes("class"))
            {
                var className = classNode.Attributes.GetAttribute<string>("name", "");
                var classModifier = GetModifierType(classNode.Attributes.GetAttribute<string>("modifier", ""));

                if (!string.IsNullOrEmpty(className))
                {
                    var element = new ClassElement(parentElement);
                    element.Elementname = className;
                    element.Modifier = classModifier;

                    parentElement.ChildElements.Add(element);
                    Logger.Normal("Parsing Class {0}", element.Elementname);

                    ParseFieldItem(classNode, element);
                }
            }
        }

        /// <summary>
        /// Analysiert eine Message Zeile
        /// </summary>
        /// <param name="namespaceNode"></param>
        /// <param name="parentElement"></param>
        private void ParseMessage(XmlNode namespaceNode, NamespaceElement parentElement)
        {
            foreach (XmlNode classNode in namespaceNode.SelectNodes("message"))
            {
                var messageName = classNode.Attributes.GetAttribute<string>("name", "");
                var messageModifier = GetModifierType(classNode.Attributes.GetAttribute<string>("modifier", ""));
                var messageCode = classNode.Attributes.GetAttribute<int>("code", Project.GetNextMessageCode());

                if (!string.IsNullOrEmpty(messageName))
                {
                    var element = new MessageElement(parentElement);
                    element.Elementname = messageName;
                    element.Modifier = messageModifier;
                    element.Code = messageCode;

                    parentElement.ChildElements.Add(element);
                    Logger.Normal("Parsing Message {0}", element.Elementname);

                    ParseFieldItem(classNode, element);
                }
            }
        }

        /// <summary>
        /// Analysiert eine EnumItem Zeile
        /// </summary>
        /// <param name="enumNode"></param>
        /// <param name="parentElement"></param>
        private void ParseEnumItem(XmlNode enumNode, EnumElement parentElement)
        {
            foreach (XmlNode enumItemNode in enumNode.SelectNodes("item"))
            {
                var itemName = enumItemNode.Attributes.GetAttribute<string>("name", "");
                var itemValue = enumItemNode.InnerText;

                if (!string.IsNullOrEmpty(itemName))
                {
                    if (!string.IsNullOrEmpty(itemValue) & !parentElement.IsFlag)
                        itemValue = parentElement.ChildElements.Count.ToString();
                    
                    int TestValue;
                    if (int.TryParse(itemValue, out TestValue))
                    {
                        var element = new EnumItemElement(parentElement);
                        element.Elementname= itemName;
                        element.Value = TestValue;

                        parentElement.ChildElements.Add(element);
                        Logger.Normal("Parsing EnumItem {0}", element.Elementname);
                    }
                }
            }
        }

        /// <summary>
        /// Analysiert eine FieldItem Zeile
        /// </summary>
        /// <param name="classNode"></param>
        /// <param name="parentElement"></param>
        private void ParseFieldItem(XmlNode classNode, Element parentElement)
        {
            foreach (XmlNode fieldNode in classNode.SelectNodes("field"))
            {
                var fieldName = fieldNode.Attributes.GetAttribute<string>("name", "");
                var fieldModifier = GetModifierType(fieldNode.Attributes.GetAttribute<string>("modifier", ""));
                var fieldType = fieldNode.Attributes.GetAttribute<string>("type", "");
                var fieldList = fieldNode.Attributes.GetAttribute<bool>("list", false);
                var fieldVirtual = fieldNode.Attributes.GetAttribute<bool>("virtual", false);

                if (!string.IsNullOrEmpty(fieldName))
                {
                    FieldElement element = null;
                    if (parentElement is ClassElement) element = new FieldElement((ClassElement)parentElement);
                    if (parentElement is MessageElement) element = new FieldElement((MessageElement)parentElement);

                    element.Type = new TypeInfo();

                    if (element != null)
                    {
                        element.Elementname = fieldName;
                        element.Modifier = fieldModifier;
                        element.Type = GetType(fieldType, element.Type);
                        element.DefaultValue = fieldNode.InnerText;
                        element.Type.IsList = fieldList;
                        element.IsVirtual = fieldVirtual;

                        parentElement.ChildElements.Add(element);
                        Logger.Normal("Parsing Field {0}", element.Elementname);
                    }
                }
            }
        }

        /// <summary>
        /// Gibt an ob das Wort ein Schlüssenwort ist
        /// </summary>
        /// <param name="word"></param>
        /// <returns></returns>
        private bool CheckKeyWord(string word)
        {
            if (string.IsNullOrEmpty(word)) return true;

            if (word == "namespace") return true;
            if (word == "class") return true;
            if (word == "enum") return true;
            if (word == "message") return true;
            if (word == "enumitem") return true;
            if (word == "field") return true;
            if (word == "private") return true;
            if (word == "public") return true;
            if (word == "internal") return true;
            if (word == "protected") return true;
            if (word == "end") return true;

            if (word == "int") return true;
            if (word == "int16") return true;
            if (word == "int32") return true;
            if (word == "int64") return true;
            if (word == "long") return true;
            if (word == "float") return true;
            if (word == "double") return true;
            if (word == "decimal") return true;
            if (word == "string") return true;
            if (word == "byte") return true;
            if (word == "bool") return true;
            if (word == "char") return true;
            if (word == "datetime") return true;
            
            return false;
        }

        /// <summary>
        /// Ermittelt den Modifizierer
        /// </summary>
        /// <param name="word"></param>
        /// <returns></returns>
        private Modifiers GetModifierType(string word)
        {
            switch (word)
            {
                case "private":
                    return Modifiers.Private;

                case "public":
                    return Modifiers.Public;

                case "internal":
                    return Modifiers.Internal;

                case "protected":
                    return Modifiers.Protected;

                default:
                    return Modifiers.Unkown;
            }
        }

        /// <summary>
        /// Ertmittelt den Datentyp
        /// </summary>
        /// <param name="word"></param>
        /// <param name="t"></param>
        /// <returns></returns>
        private TypeInfo GetType(string word, TypeInfo t)
        {
            switch (word)
            {
                case "int16":
                    t.Type = Types.INT16;
                    break;

                case "int":
                case "int32":
                    t.Type = Types.INT;
                    break;

                case "int64":
                    t.Type = Types.INT64;
                    break;

                case "long":
                    t.Type = Types.LONG;
                    break;

                case "float":
                    t.Type = Types.FLOAT;
                    break;

                case "double":
                    t.Type = Types.DOUBLE;
                    break;

                case "string":
                    t.Type = Types.STRING;
                    break;

                case "byte":
                    t.Type = Types.BYTE;
                    break;

                case "datetime":
                    t.Type = Types.DATETIME;
                    break;

                case "bool":
                    t.Type = Types.BOOL;
                    break;

                case "decimal":
                    t.Type = Types.DECIMAL;
                    break;

                case "char":
                    t.Type = Types.CHAR;
                    break;

                default:
                    var RefType = Project.FindReferenceType(word);

                    t.Type = Types.REFERENCE;
                    t.ReferenceName = word;
                    break;
            }

            return t;
        }

        //private bool TryToConvert(string Word, TypeInfo T)
        //{
        //    switch (T.Type)
        //    {
        //        case TypeEnum.INT16:
        //            Int16 Var1;
        //            return Int16.TryParse(Word, out Var1);
        //        case TypeEnum.INT:
        //        case TypeEnum.INT32:
        //            int Var2;
        //            return int.TryParse(Word, out Var2);
        //        case TypeEnum.INT64:
        //        case TypeEnum.LONG:
        //            long Var3;
        //            return long.TryParse(Word, out Var3);
        //        case TypeEnum.FLOAT:
        //            float Var4;
        //            return float.TryParse(Word, out Var4);
        //        case TypeEnum.DOUBLE:
        //            double Var5;
        //            return double.TryParse(Word, out Var5);
        //        case TypeEnum.DECIMAL:
        //            decimal Var6;
        //            return decimal.TryParse(Word, out Var6);
        //        case TypeEnum.STRING:
        //            return true;
        //        case TypeEnum.BYTE:
        //            byte Var7;
        //            return byte.TryParse(Word, out Var7);
        //        case TypeEnum.BOOL:
        //            bool Var8;
        //            return bool.TryParse(Word, out Var8);
        //        case TypeEnum.CHAR:
        //            return true;
        //        case TypeEnum.DATETIME:
        //            DateTime Var9;
        //            return DateTime.TryParse(Word, out Var9);
        //        default:
        //            return false;
        //    }
        //}
  
        /// <summary>
        /// Verknüft referenzierte Typen
        /// </summary>
        private void Recognizer()
        {
            Logger.Normal("Recognize Object Reference...");

            var elementStack = new Stack<Element>();

            elementStack.Push(Project);

            foreach (var item in Project.IncludeFiles)
                elementStack.Push(item.Project);

            while (elementStack.Count > 0)
            {
                var Element = elementStack.Pop();

                foreach (var ChildElement in Element.ChildElements)
                {
                    elementStack.Push(ChildElement);
                }

                if (Element.Type != null && Element.Type.Type == Types.REFERENCE)
                {
                    var RefElement = Project.FindReferenceType(Element.Type.ReferenceName);

                    if (RefElement == null)
                    {
                        AddWarning(0, "Referenz {0} -> {1} wurde nicht gefunden", Element.Elementname, Element.Type.ReferenceName);
                        Logger.Warning( "Referenz {0} -> {1} wurde nicht gefunden", Element.Elementname, Element.Type.ReferenceName);
                        return;
                    }

                    Element.Type.ReferenceElement = RefElement;

                    Logger.Normal("Object Reference {0} -> {1}", Element.Elementname, RefElement.Elementname);
                }
            }

            Logger.Normal("...Complet");
        }

        /// <summary>
        /// Fügt der Fehlerliste eine Information hinzu
        /// </summary>
        /// <param name="row"></param>
        /// <param name="format"></param>
        /// <param name="args"></param>
        internal void AddInformation(int row, string format, params Object[] args)
        {
            ErrorList.Add(new MessageItem() { Message = string.Format(format, args), MessageType = MessageTypes.Information, Row = row });
        }

        /// <summary>
        /// Fügt der Fehlerliste eine Warnung hinzu
        /// </summary>
        /// <param name="row"></param>
        /// <param name="format"></param>
        /// <param name="args"></param>
        internal void AddWarning(int row, string format, params Object[] args)
        {
            ErrorList.Add(new MessageItem() { Message = string.Format(format, args), MessageType = MessageTypes.Warning, Row = row });
        }

        /// <summary>
        /// Fügt der Fehlerliste einen Fehler hinzu
        /// </summary>
        /// <param name="row"></param>
        /// <param name="format"></param>
        /// <param name="args"></param>
        internal void AddException(int row, string format, params Object[] args)
        {
            ErrorList.Add(new MessageItem() { Message = string.Format(format, args), MessageType = MessageTypes.Exception, Row = row });
        }
              
        /// <summary>
        /// Liefert die Quelldatei
        /// </summary>
        public FileInfo SourceFile { get; private set; }

        /// <summary>
        /// Liefert das Projekt
        /// </summary>
        public ProjectElement Project { get { return _project; } }

        /// <summary>
        /// Liefert den Basisquellcode Parser
        /// </summary>
        /// <remarks>Wird für Include Dateien verwendet</remarks>
        public IOpGenParser BaseParser { get { return _baseParser; } }

        /// <summary>
        /// Liefert die Fehlerliste
        /// </summary>
        public MessageItemCollection ErrorList { get { return _errorList; } }

        /// <summary>
        /// Liefert die aktuelle Zeile
        /// </summary>
        private int Row { get; set; }
    }
}
